package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Slf4j
@Component
public class CacheClient {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 将任意类型Java对象序列化为JSON，并保存到String类型的key Redis中，并且可以设置过期时间
     * @param key 缓存的key
     * @param value 缓存的数据
     * @param time 缓存的时间
     * @param unit 时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }
    /**
     * 将任意类型Java对象序列化为JSON，并保存到String类型的key Redis中，并且可以设置逻辑过期时间
     * @param key 缓存的key
     * @param value 缓存的数据
     * @param time 缓存的时间
     * @param unit 时间单位
     */
    public void setLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        //不知道传入的Object value中有没有逻辑过期字段，我们自己写入逻辑过期字段
        RedisData redisData = new RedisData();
        redisData.setData(value);
        //按照给定的时间单位转成秒(unit.toSeconds(time))
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    /**
     * 根据key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
     * @param keyPrefix 缓存的前缀
     * @param type 缓存的数据类型
     * @return 缓存的数据
     */
    public <R,ID> R queryNullCachePenetration(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        //1.从redis缓存中查询
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否命中
        if (StrUtil.isNotBlank(json)) {
            //3.命中返回商铺信息
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        //判断商铺信息是否为空
        if (json != null) {
            return null;
        }
        // 4. 未命中，
        // 查询数据库
        R r = dbFallback.apply(id);
        // 数据库不存在，返回错误
        if (r == null) {
            set(key, "", time, unit);
            return null;
        }
        // 数据库存在，写入redis缓存
        set(key, JSONUtil.toJsonStr(r),time, unit);
        return r;
    }
    /**
     * 根据key查询缓存，并反序列化为指定类型，利用设置逻辑过期时间的方式解决缓存击穿问题
     * @param keyPrefix 缓存的前缀
     * @param type 缓存的数据类型
     * @return 缓存的数据
     */
    public <R,ID> R queryLogicalCacheBreakdown(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        //1.从redis缓存中查询
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否命中
        if (StrUtil.isBlank(json)) {
            //未命中，返回错误
            return null;
        }
        //3.命中
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject)redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //3.1判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //3.2未过期，返回数据
            return r;
        }
        //3.3过期，获取锁
        String lockKey = LOCK_SHOP_KEY + id;
        if (!tryLock(lockKey)) {
            //3.3.1获取锁失败，返回数据
            return r;
        }
        //3.3.2获取锁成功，二次检查缓存是否过期，开启独立线程，新线程缓存重建，旧线程返回数据
        String renewJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(renewJson)) {
            //3.3.2.1二次检查缓存未过期，返回数据
            RedisData renewRedisData = JSONUtil.toBean(renewJson, RedisData.class);
            JSONObject renewData = (JSONObject)renewRedisData.getData();
            R renewr = JSONUtil.toBean(renewData, type);
            LocalDateTime renewExpireTime = renewRedisData.getExpireTime();
            if (renewExpireTime.isAfter(LocalDateTime.now())) {
                //3.2未过期，返回数据
                return renewr;
            }
        }
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //重建缓存
                R renewr = dbFallback.apply(id);
               setLogicalExpire(key, renewr, 20L, TimeUnit.SECONDS);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unLock(lockKey);
            }
        });
        return r;
    }
    /**
     * 根据id查询商铺信息，使用Synchronized关键字加锁，解决缓存击穿CacheBreakdown问题
     * @param id 商铺id
     * @return 商铺详情数据
     */
    public <R,ID> R querySynchronizedCacheBreakdown(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        String key=keyPrefix + id;
        //1.从redis缓存中查询
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否命中
        if (StrUtil.isNotBlank(json)) {
            //3.命中返回商铺信息
            R r= JSONUtil.toBean(json, type);
            return r;
        }
        // 4. 未命中，使用synchronized关键字加锁
        synchronized (id.toString().intern()) {
            //二次检查缓存是否命中，返回数据
            String renewJson = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(renewJson)) {
                //3.命中返回商铺信息
                R r= JSONUtil.toBean(renewJson, type);
                return r;
            }
            // 查询数据库
            R r = dbFallback.apply(id);
            // 数据库存在，写入redis缓存
            set(key, JSONUtil.toJsonStr(r), time, unit);
            return r;
        }
    }
    /**
     * 根据id查询商铺信息，使用setIfAbsent加锁,解决缓存击穿CacheBreakdown问题
     * @param id 商铺id
     * @return 商铺详情数据
     */
    public  <R,ID> R querySetIfAbsentCacheBreakdown(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit) {
        String key=keyPrefix + id;
        //1.从redis缓存中查询
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断缓存是否命中
        if (StrUtil.isNotBlank(json)) {
            //3.命中返回商铺信息
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        // 4. 未命中，使用setIfAbsent加锁
        //4.1获取互斥锁
        String lockKey = "lock:shop:" + id;
        //4.2判断是否获取锁成功
        R r=null;
        try {
            if (!tryLock(lockKey)) {
                //4.2.获取锁失败，休眠一会，重新尝试
                Thread.sleep(50);
                return querySetIfAbsentCacheBreakdown(keyPrefix, id, type, dbFallback,time, unit);
            }
            // 查询数据库
            r = dbFallback.apply(id);
            Thread.sleep(200);
            set(key, JSONUtil.toJsonStr(r), time, unit);
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally {
            unLock(lockKey);
        }
        return r;
    }
    /**
     * 创建线程池，用于获取锁成功之后，开启新线程进行缓存重建
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**获取锁
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //不能直接返回flag，因为flag为Boolean类型可以为null，方法要求返回boolean类型不可以为null，
        //直接返回会出现拆箱，会出现空指针异常
        return BooleanUtil.isTrue(flag);
    }
    /**释放锁
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

}
